/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.compatibility.core.transport;

import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.mule.compatibility.core.api.endpoint.ImmutableEndpoint;
import org.mule.compatibility.core.endpoint.MuleEndpointURI;
import org.mule.compatibility.core.transport.MuleAbstractTransportMessageHandlerTestCase.MethodInvocation.MethodPart;
import org.mule.runtime.core.api.MuleContext;
import org.mule.runtime.core.api.MuleException;
import org.mule.runtime.core.api.context.WorkManager;
import org.mule.runtime.core.api.retry.RetryCallback;
import org.mule.runtime.core.api.retry.RetryPolicyTemplate;

import java.util.List;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;

import edu.umd.cs.mtc.MultithreadedTestCase;
import edu.umd.cs.mtc.TestFramework;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This test tests class {@link AbstractTransportMessageHandler} but its name starts with "Mule" because there is an exclusion
 * rule in parent pom for test classes that have their names starting with "Abstract".
 */
public class MuleAbstractTransportMessageHandlerTestCase {

  /**
   * The logger used for this class
   */
  static Logger log = LoggerFactory.getLogger(MuleAbstractTransportMessageHandlerTestCase.class);

  @Test
  public void testStartRethrowsMuleExceptionCorrectly() throws Exception {
    final MuleException someMuleException = mock(MuleException.class);
    AbstractTransportMessageHandler connectable = new AbstractTransportMessageHandler(createDummyEndpoint()) {

      @Override
      protected void doStart() throws MuleException {
        throw someMuleException;
      }

      @Override
      protected WorkManager getWorkManager() {
        return null;
      }

      @Override
      protected ConnectableLifecycleManager createLifecycleManager() {
        return new ConnectableLifecycleManager("test", this);
      }
    };
    connectable.initialise();
    connectable.connect();
    try {
      connectable.start();
      fail("Should have thrown a " + MuleException.class.getSimpleName());
    } catch (MuleException caughtException) {
      assertExceptionIsInCaughtException(someMuleException, caughtException);
    }
  }

  /**
   * This test tests that start method is thread safe and that {@link AbstractTransportMessageHandler#doStart()} is always called
   * making sure that the connectable is {@link AbstractTransportMessageHandler#isConnected() connected}.
   * <p>
   * To make multithreaded test easier it uses a library called
   * <a href="http://www.cs.umd.edu/projects/PL/multithreadedtc/overview.html" >MultithreadedTC</a>. You will perhaps go and read
   * that link if you want to understand how this test works.
   * 
   * @throws Throwable
   */
  @Ignore
  @Test
  public void testStartIsThreadSafe() throws Throwable {
    TestFramework.runOnce(new AbstractConnectableMultithreaded());
  }

  /**
   * This inner class will do the work of {@link MuleAbstractTransportMessageHandlerTestCase#testStartIsThreadSafe()
   * testStartIsThreadSafe()}.
   */
  @Ignore
  class AbstractConnectableMultithreaded extends MultithreadedTestCase {

    private volatile AbstractConnectableForTest connectable;

    /**
     * This is called before any of the thread* methods are called. Pretty much like a {@link Before} method in JUnit 4.
     */
    @Override
    public void initialize() {
      try {
        ImmutableEndpoint endpoint = createDummyEndpoint();

        connectable = new AbstractConnectableForTest(endpoint);
        connectable.initialise();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    /**
     * This will be called by the <a href="http://www.cs.umd.edu/projects/PL/multithreadedtc/overview.html" >MultithreadedTC</a>
     * framework with an independent thread.
     * 
     * @throws Exception on unexpected error.
     */
    public void thread1() throws Exception {
      connectable.start();
    }

    /**
     * @see #thread1()
     * @throws Exception on unexpected error
     */
    public void thread2() throws Exception {
      // waiting for first tick means that thread1 will work on tick 0 and
      // after it blocks then this one will start.
      waitForTick(1);
      connectable.start();
    }

    /**
     * This is called after all the thread* methods finish. Pretty much like an {@link After} method in JUnit 4.
     */
    @Override
    public void finish() {
      for (MethodInvocation methodInvocation : connectable.methodInvocations) {
        log.debug(methodInvocation.toString());
      }

      int i = 0;
      assertEquals("doConnect", connectable.methodInvocations.get(i++).getMethodName());
      assertEquals("doConnect", connectable.methodInvocations.get(i++).getMethodName());
      assertEquals("doStart", connectable.methodInvocations.get(i++).getMethodName());
      assertEquals("doStart", connectable.methodInvocations.get(i++).getMethodName());
    }

    /**
     * This is a specific implementation of {@link AbstractTransportMessageHandler} for this test that will only implement
     * {@link #doConnect()} and {@link #doStart()} methods.
     * <p>
     * The implementation of those methods simulate a long {@link #doConnect()} execution that makes the other thread attempt to
     * execute {@link #doStart()}. They have also code that validates that {@link #doStart()} is never called before
     * {@link #doConnect()} has finished.
     */
    @Ignore
    class AbstractConnectableForTest extends AbstractTransportMessageHandler {

      private final AtomicBoolean doConnectCalled = new AtomicBoolean();
      private final AtomicBoolean doStartCalled = new AtomicBoolean();

      /**
       * This list will hold reference to each of the calls to the methods {@link #doConnect()} and {@link #doStart()}. We use a
       * {@link Vector} because it will be accessed by different threads.
       */
      List<MethodInvocation> methodInvocations = new Vector<MethodInvocation>();

      public AbstractConnectableForTest(ImmutableEndpoint endpoint) {
        super(endpoint);
      }

      @Override
      protected WorkManager getWorkManager() {
        return null;
      }

      @Override
      protected ConnectableLifecycleManager createLifecycleManager() {
        return new ConnectableLifecycleManager("test", this);
      }

      @Override
      protected void doConnect() throws Exception {
        methodInvocations.add(new MethodInvocation(Thread.currentThread(), "doConnect", MethodPart.BEGINNING));
        assertTrue(doConnectCalled.compareAndSet(false, true));
        assertFalse(doStartCalled.get());

        // This method is called by thread1 in the test and waiting
        // for tick 2 will make thread 2 execute during tick 1, attempting
        // to call doStart(). Thread 2 should be then blocked waiting on a
        // monitor until AbstractConnectable.connected is true, which
        // triggers tick 2 and thread 1 will resume its execution,
        // finishing the exection of this method.
        waitForTick(2);

        assertFalse(doStartCalled.get());

        methodInvocations.add(new MethodInvocation(Thread.currentThread(), "doConnect", MethodPart.END));
      }

      @Override
      protected void doStart() throws MuleException {
        methodInvocations.add(new MethodInvocation(Thread.currentThread(), "doStart", MethodPart.BEGINNING));
        assertTrue(doStartCalled.compareAndSet(false, true));
        assertTrue(doConnectCalled.get());
        methodInvocations.add(new MethodInvocation(Thread.currentThread(), "doStart", MethodPart.END));
      }

    }
  }

  /**
   * This class just represent a method invocation that allow keeping track of the order in which calls are made by different
   * threads.
   */
  @org.junit.Ignore
  static class MethodInvocation {

    @Ignore
    static enum MethodPart {
      BEGINNING, END
    }

    private final Thread thread;
    private final String methodName;
    private final MethodPart methodPart;

    public MethodInvocation(Thread thread, String methodName, MethodPart methodPart) {
      this.thread = thread;
      this.methodName = methodName;
      this.methodPart = methodPart;
    }

    public Thread getThread() {
      return thread;
    }

    public String getMethodName() {
      return methodName;
    }

    public MethodPart getMethodPart() {
      return methodPart;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      } else if (obj == null || obj.getClass() != this.getClass()) {
        return false;
      } else {
        MethodInvocation other = (MethodInvocation) obj;
        return new EqualsBuilder().append(this.thread, other.thread).append(this.methodName, other.methodName)
            .append(this.methodPart, other.methodPart).isEquals();
      }
    }

    @Override
    public int hashCode() {
      return new HashCodeBuilder().append(this.thread).append(this.methodName).append(this.methodPart).toHashCode();
    }

    @Override
    public String toString() {
      return "Thread " + this.thread + " passing through " + this.methodName + "() at the " + this.methodPart;
    }
  }

  private void assertExceptionIsInCaughtException(MuleException someMuleException, MuleException caughtException) {
    boolean found = false;
    Throwable candidate = caughtException;
    while (candidate != null) {
      if (someMuleException.equals(candidate)) {
        found = true;
        break;
      }

      candidate = candidate.getCause();
    }

    if (found == false) {
      fail();
    }
  }

  /**
   * @return an dummy implementation of {@link ImmutableEndpoint} suitable for this test.
   * @throws Exception
   */
  ImmutableEndpoint createDummyEndpoint() throws Exception {
    ImmutableEndpoint endpoint = mock(ImmutableEndpoint.class);
    MuleContext muleContext = mock(MuleContext.class);
    when(endpoint.getEndpointURI()).thenReturn(new MuleEndpointURI("http://dummy.endpoint/", muleContext));
    AbstractConnector connector = mock(AbstractConnector.class);
    when(endpoint.getConnector()).thenReturn(connector);

    RetryPolicyTemplate retryPolicyTemplate = mock(RetryPolicyTemplate.class);
    when(endpoint.getRetryPolicyTemplate()).thenReturn(retryPolicyTemplate);
    when(retryPolicyTemplate.execute(any(RetryCallback.class), any(WorkManager.class))).thenAnswer(new Answer<Object>() {

      @Override
      public Object answer(InvocationOnMock invocation) throws Throwable {
        RetryCallback retryCallback = (RetryCallback) invocation.getArguments()[0];
        retryCallback.doWork(null);
        return null;
      }
    });

    return endpoint;
  }
}
